﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.ToolTip;

namespace LightCAD.MathLib.CSG
{
   public class Vec3
    {
        #region scope properties or methods

        #endregion

        #region Properties

        public double x;
        public double y;
        public double z;

        #endregion

        #region constructor
        public Vec3() : this(0, 0, 0)
        { }
        public Vec3(Vec3 v) : this(v.x, v.y, v.z)
        { }
        public Vec3(double x = 0, double y = 0, double z = 0)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        public Vec3(Vec2 v2, double z = 0)
        {
            this.x = v2?.x ?? 0;
            this.y = v2?.y ?? 0;
            this.z = z;
        }
        #endregion

        #region methods
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Set(double x, double y, double z = double.NaN)
        {
            if (z == double.NaN) z = this.z; // sprite.scale.set(x,y)
            this.x = x;
            this.y = y;
            this.z = z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetScalar(double scalar)
        {
            this.x = scalar;
            this.y = scalar;
            this.z = scalar;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetX(double x)
        {
            this.x = x;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetY(double y)
        {
            this.y = y;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetZ(double z)
        {
            this.z = z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetComponent(double index, double value)
        {
            switch (index)
            {
                case 0: this.x = value; break;
                case 1: this.y = value; break;
                case 2: this.z = value; break;
                default: throw new Error("index is out of range: " + index);
            }
            return this;
        }
        public double GetComponent(double index)
        {
            switch (index)
            {
                case 0: return this.x;
                case 1: return this.y;
                case 2: return this.z;
                default: throw new Error("index is out of range: " + index);
            }
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 clone()
        {
            return new Vec3(this.x, this.y, this.z);
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 copy(Vec3 v)
        {
            this.x = v.x;
            this.y = v.y;
            this.z = v.z;
            return this;
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Add(Vec3 v)
        {
            this.x += v.x;
            this.y += v.y;
            this.z += v.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 AddScalar(double s)
        {
            this.x += s;
            this.y += s;
            this.z += s;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 AddVectors(Vec3 a, Vec3 b)
        {
            this.x = a.x + b.x;
            this.y = a.y + b.y;
            this.z = a.z + b.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 AddScaledVector(Vec3 v, double s)
        {
            this.x += v.x * s;
            this.y += v.y * s;
            this.z += v.z * s;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Sub(Vec3 v)
        {
            this.x -= v.x;
            this.y -= v.y;
            this.z -= v.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SubScalar(double s)
        {
            this.x -= s;
            this.y -= s;
            this.z -= s;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SubVectors(Vec3 a, Vec3 b)
        {
            this.x = a.x - b.x;
            this.y = a.y - b.y;
            this.z = a.z - b.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Mul(Vec3 v)
        {
            this.x *= v.x;
            this.y *= v.y;
            this.z *= v.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 MulScalar(double scalar)
        {
            this.x *= scalar;
            this.y *= scalar;
            this.z *= scalar;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 MulVectors(Vec3 a, Vec3 b)
        {
            this.x = a.x * b.x;
            this.y = a.y * b.y;
            this.z = a.z * b.z;
            return this;
        }





        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Div(Vec3 v)
        {
            this.x /= v.x;
            this.y /= v.y;
            this.z /= v.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 DivScalar(double scalar)
        {
            return this.MulScalar(1 / scalar);
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Min(Vec3 v)
        {
            this.x = Math.Min(this.x, v.x);
            this.y = Math.Min(this.y, v.y);
            this.z = Math.Min(this.z, v.z);
            return this;
        }

        [MethodImpl((MethodImplOptions)768)]
        public double Min()
        {
            return MathEx.Min(this.x, this.y,this.z);
        }
        [MethodImpl((MethodImplOptions)768)]
        public double Max()
        {
            return MathEx.Max(this.x, this.y,this.z);
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Max(Vec3 v)
        {
            this.x = Math.Max(this.x, v.x);
            this.y = Math.Max(this.y, v.y);
            this.z = Math.Max(this.z, v.z);
            return this;
        }
    
        public int maxDim()
        {
            return (this.x >= this.y) ? ((this.x >= this.z) ? 0 : 2) :
                                     ((this.y >= this.z) ? 1 : 2);
        }
        public int minDim()
        {
            return (this.x <= this.y) ? ((this.x <= this.z) ? 0 : 2) :
                                     ((this.y <= this.z) ? 1 : 2);
        }



        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Clamp(Vec3 min, Vec3 max)
        {
            // assumes min < max, componentwise
            this.x = Math.Max(min.x, Math.Min(max.x, this.x));
            this.y = Math.Max(min.y, Math.Min(max.y, this.y));
            this.z = Math.Max(min.z, Math.Min(max.z, this.z));
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 ClampScalar(double minVal, double maxVal)
        {
            this.x = Math.Max(minVal, Math.Min(maxVal, this.x));
            this.y = Math.Max(minVal, Math.Min(maxVal, this.y));
            this.z = Math.Max(minVal, Math.Min(maxVal, this.z));
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 ClampLength(double min, double max)
        {
            double length = this.Length();
            return this.DivScalar(length == 0 ? 1 : length).MulScalar(Math.Max(min, Math.Min(max, length)));
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Floor()
        {
            this.x = Math.Floor(this.x);
            this.y = Math.Floor(this.y);
            this.z = Math.Floor(this.z);
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Ceil()
        {
            this.x = Math.Ceiling(this.x);
            this.y = Math.Ceiling(this.y);
            this.z = Math.Ceiling(this.z);
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Round()
        {
            this.x = Math.Round(this.x);
            this.y = Math.Round(this.y);
            this.z = Math.Round(this.z);
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 RoundToZero()
        {
            this.x = (this.x < 0) ? Math.Ceiling(this.x) : Math.Floor(this.x);
            this.y = (this.y < 0) ? Math.Ceiling(this.y) : Math.Floor(this.y);
            this.z = (this.z < 0) ? Math.Ceiling(this.z) : Math.Floor(this.z);
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Negate()
        {
            this.x = -this.x;
            this.y = -this.y;
            this.z = -this.z;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public double Dot(Vec3 v)
        {
            return this.x * v.x + this.y * v.y + this.z * v.z;
        }
        [MethodImpl((MethodImplOptions)768)]
        public double LengthSq()
        {
            return this.x * this.x + this.y * this.y + this.z * this.z;
        }
        [MethodImpl((MethodImplOptions)768)]
        public double Length()
        {
            return Math.Sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
        }
        [MethodImpl((MethodImplOptions)768)]
        public double ManhattanLength()
        {
            return Math.Abs(this.x) + Math.Abs(this.y) + Math.Abs(this.z);
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Normalize()
        {
            var len = this.Length();

            return this.DivScalar(len == 0 ? 1 : len);
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetLength(double length)
        {
            return this.Normalize().MulScalar(length);
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Lerp(Vec3 v, double alpha)
        {
            this.x += (v.x - this.x) * alpha;
            this.y += (v.y - this.y) * alpha;
            this.z += (v.z - this.z) * alpha;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 LerpVectors(Vec3 v1, Vec3 v2, double alpha)
        {
            this.x = v1.x + (v2.x - v1.x) * alpha;
            this.y = v1.y + (v2.y - v1.y) * alpha;
            this.z = v1.z + (v2.z - v1.z) * alpha;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Cross(Vec3 v)
        {
            return this.CrossVectors(this, v);
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 CrossVectors(Vec3 a, Vec3 b)
        {
            double ax = a.x, ay = a.y, az = a.z;
            double bx = b.x, by = b.y, bz = b.z;
            this.x = ay * bz - az * by;
            this.y = az * bx - ax * bz;
            this.z = ax * by - ay * bx;
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 ProjectOnVector(Vec3 v)
        {
            double denominator = v.LengthSq();
            if (denominator == 0) return this.Set(0, 0, 0);
            double scalar = v.Dot(this) / denominator;
            return this.copy(v).MulScalar(scalar);
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Reflect(Vec3 normal)
        {
            // reflect incident vector off plane orthogonal to normal
            // normal is assumed to have unit length
            return this.Sub(new Vec3().copy(normal).MulScalar(2 * this.Dot(normal)));
        }
        [MethodImpl((MethodImplOptions)768)]
        public double AngleTo(Vec3 v)
        {
            double denominator = Math.Sqrt(this.LengthSq() * v.LengthSq());
            if (denominator == 0) return MathEx.PI / 2;
            double theta = this.Dot(v) / denominator;
            // clamp, to handle numerical problems
            return Math.Acos(MathEx.Clamp(theta, -1, 1));
        }
        [MethodImpl((MethodImplOptions)768)]
        public double DistanceTo(Vec3 v)
        {
            return Math.Sqrt(this.DistanceToSquared(v));
        }
        [MethodImpl((MethodImplOptions)768)]
        public double DistanceToSquared(Vec3 v)
        {
            double dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;
            return dx * dx + dy * dy + dz * dz;
        }
        [MethodImpl((MethodImplOptions)768)]
        public double ManhattanDistanceTo(Vec3 v)
        {
            return Math.Abs(this.x - v.x) + Math.Abs(this.y - v.y) + Math.Abs(this.z - v.z);
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetFromSphericalCoords(double radius, double phi, double theta)
        {
            double sinPhiRadius = Math.Sin(phi) * radius;
            this.x = sinPhiRadius * Math.Sin(theta);
            this.y = Math.Cos(phi) * radius;
            this.z = sinPhiRadius * Math.Cos(theta);
            return this;
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 SetFromCylindricalCoords(double radius, double theta, double y)
        {
            this.x = radius * Math.Sin(theta);
            this.y = y;
            this.z = radius * Math.Cos(theta);
            return this;
        }


        [MethodImpl((MethodImplOptions)768)]
        public bool Equals(Vec3 v)
        {
            return ((v.x == this.x) && (v.y == this.y) && (v.z == this.z));
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 FromArray(double[] array, int offset = 0)
        {
            this.x = array[offset];
            this.y = array[offset + 1];
            this.z = array[offset + 2];
            return this;
        }

        [MethodImpl((MethodImplOptions)768)]
        public double[] ToArray(double[] array = null, int offset = 0)
        {
            if (array == null) array = new double[3];
            array[offset] = this.x;
            array[offset + 1] = this.y;
            array[offset + 2] = this.z;
            return array;
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec2 ToVector2(string xname = null, string yname = null)
        {
            if (xname == null) xname = "x";
            if (yname == null) yname = "y";

            var xv = this.x;
            if (xname == "y") xv = this.y;
            else if (xname == "z") xv = this.z;

            var yv = this.y;
            if (yname == "x") yv = this.x;
            else if (xname == "z") yv = this.z;

            return new Vec2(xv, yv);
        }

        [MethodImpl((MethodImplOptions)768)]
        public Vec3 Random()
        {
            this.x = MathEx.Random();
            this.y = MathEx.Random();
            this.z = MathEx.Random();
            return this;
        }
        [MethodImpl((MethodImplOptions)768)]
        public Vec3 RandomDirection()
        {
            // Derived from https://mathworld.wolfram.com/SpherePointPicking.html
            double u = (MathEx.Random() - 0.5) * 2;
            double t = MathEx.Random() * MathEx.PI * 2;
            double f = Math.Sqrt(1 - u * 2);
            this.x = f * Math.Cos(t);
            this.y = f * Math.Sin(t);
            this.z = u;
            return this;
        }


        #endregion


        [MethodImpl((MethodImplOptions)768)]
        public override bool Equals(object obj)
        {
            return this.Equals(obj as Vec3);
        }

        [MethodImpl((MethodImplOptions)768)]
        public override string ToString()
        {
            return $"[{(float)this.x},{(float)this.y},{(float)this.z}]";
        }


        public double this[int i]
        {
            get
            {
                if (i == 0)
                    return this.x;
                else if (i == 1)
                    return this.y;
                else if (i == 2)
                    return this.z;
                else
                    throw new Exception("error index");
            }
            set
            {
                if (i == 0)
                    this.x = value;
                else if (i == 1)
                    this.y = value;
                else if (i == 2)
                    this.z = value;
                else
                    throw new Exception("error index");
            }
        }

        [MethodImpl((MethodImplOptions)768)]
        public static bool operator ==(Vec3 lhs, Vec3 rhs)
        {
            return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z;
        }
        [MethodImpl((MethodImplOptions)768)]
        public static bool operator !=(Vec3 lhs, Vec3 rhs)
        {
            return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z;
        }

        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator +(Vec3 a, Vec3 b)
        {
            return new Vec3(a.x + b.x, a.y + b.y, a.z + b.z);
        }

        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator -(Vec3 a, Vec3 b)
        {
            return new Vec3(a.x - b.x, a.y - b.y, a.z - b.z);
        }

        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator -(Vec3 a)
        {
            return new Vec3(-a.x, -a.y, -a.z);
        }

        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator *(Vec3 a, double d)
        {
            return new Vec3(a.x * d, a.y * d, a.z * d);
        }
        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator *(double d, Vec3 a)
        {
            return new Vec3(a.x * d, a.y * d, a.z * d);
        }
        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator /(Vec3 a, double d)
        {
            return new Vec3(a.x / d, a.y / d, a.z / d);
        }

        [MethodImpl((MethodImplOptions)768)]
        public static Vec3 operator *(Vec3 left, Vec3 right)
        {
            return new Vec3(
                left.x * right.x,
                left.y * right.y,
                left.z * right.z
            );
        }
        public static Vec3 abs(Vec3 n)
        {
            var x = Math.Abs(n.x);
           var y = Math.Abs(n.y);
            var z = Math.Abs(n.z);
            return new Vec3(x,y,z);
        }
        // component-wise max/min
        public static Vec3 max(Vec3 lhs, Vec3 rhs)
        {
            return new Vec3(Math.Max(lhs.x, rhs.x),
                            Math.Max(lhs.y, rhs.y),
                            Math.Max(lhs.z, rhs.z));
        }
        public static Vec3 min(Vec3 lhs, Vec3 rhs)
        {
            return new Vec3(Math.Min(lhs.x, rhs.x),
                            Math.Min(lhs.y, rhs.y),
                            Math.Min(lhs.z, rhs.z));
        }
        public static double det(Vec3 v0, Vec3 v1, Vec3 v2)
        {
            double xy = v0.x * v1.y - v0.y * v1.x;
            double xz = v0.x * v1.z - v0.z * v1.x;
            double yz = v0.y * v1.z - v0.z * v1.y;
            return xy * v2.z - xz * v2.y + yz * v2.x;
        }
        public static double dot(Vec3 lhs, Vec3 rhs)
        {
            return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
        }

        public static Vec3 cross(Vec3 lhs, Vec3 rhs)
        {
            return new Vec3(lhs.y * rhs.z - lhs.z * rhs.y,
                           lhs.z * rhs.x - lhs.x * rhs.z,
                           lhs.x * rhs.y - lhs.y * rhs.x);
        }
    }
}
